# Table of Contents

# @PersistenceContext, @PersistenceUnit

Hibernate에서는 보통 EntityManager객체를 직접 생성하고 관리한다.

// EntityMangerFactory 생성
EntityManagerFactory entityManagerfactory = Persistence.createEntityManagerFactory("test_persistence");

// EntityManager 생성
EntityManager entityManager = entityManagerfactory.createEntityManager();

// Transaction 생성
EntityTransaction transaction = entityManager.getTransaction();

try {
    // Transaction 생성
    transaction.begin();

    // 엔티티 생성
    MemberEntity member = new MemberEntity("paul@gmail.com", "paul", "1234");

    // 데이터 삽입
    entityManager.persist(member);

    // Commit
    transaction.commit();

} catch (Exception e) {
    transaction.rollback();
} finally {
    entityManager.close();
}

entityManagerfactory.close();

Spring Data JPA를 사용하면 스프링이 직접 EntityManager를 생성하고 관리한다. 이렇게 Spring IoC Container에서 관리하는 EntityManager를 Shared EntityManager 또는 Managed EntityManager라고 한다. @PersistenceContext 어노테이션을 사용하면 이 EntityManager를 주입받을 수 있다.






 
 

























import javax.persistence.PersistenceContext;

@Service
public class MemberService {

    @PersistenceContext
    EntityManager entityManager;

    public void join(String email, String name, String password) {
        // Transaction 생성
        EntityTransaction transaction = entityManager.getTransaction();

        try {
            // Transaction 생성
            transaction.begin();

            // 엔티티 생성
            MemberEntity member = new MemberEntity("paul@gmail.com", "paul", "1234");

            // 데이터 삽입
            entityManager.persist(member);

            // Commit
            transaction.commit();

    } catch (Exception e) {
        transaction.rollback();
    } finally {
        entityManager.close();
    }
}

위 코드를 실행하면 다음과 같은 에러가 발생한다.

Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead.

Shared EntityManager는 스프링이 지정한 방법인 @Transactional 어노테이션으로 트랜잭션을 처리해야한다.







 










@Service
public class MemberService {

    @PersistenceContext
    EntityManager entityManager;

    @Transactional
    public void join(String email, String name, String password) {

        // 엔티티 생성
        MemberEntity member = new MemberEntity("paul@gmail.com", "paul", "1234");

        // 데이터 삽입
        entityManager.persist(member);
    }
}

다음과 같이 flush()를 호출하여 데이터베이스에 즉시 반영할 수도 있다.

// 엔티티 생성
MemberEntity member = new MemberEntity("paul@gmail.com", "paul", "1234");

// 데이터 삽입
entityManager.persist(member);

// 플러시
entityManager.flush();

참고로 EntityManagerFactory를 주입받으려면 @PersistenceUnit을 사용하면 된다.

@PersistenceUnit
EntityManagerFactory entityManagerFactory;

# @Transactional

스프링 프레임워크는 서블릿 컨테이너 위에서 동작한다. 서블릿 컨테이너는 사용자가 HTTP 요청을 보낼 때마다 서블릿에 하나의 스레드를 할당하며, 이 스레드가 각각의 요청을 처리하게 된다. 이 말은 스프링 프레임워크도 멀티 스레드로 동작하며, 각 스레드에서 EntityManager로 메모리에 위치한 영속성 컨텍스트에 동시에 접근할 수 있기 때문에 Thread Safe하지 않다는 것을 의미한다.

따라서 Shared EntityManager를 사용할 때는 스레드를 동기화하고 Thread Safe를 보장하기 위해 반드시 @Transactional 어노테이션을 추가해야한다.










 






import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;

@Service
public class MemberService {

    @PersistenceContext
    EntityManager entityManager;

    @Transactional
    public void join(String email, String name, String password) {
        MemberEntity member = new MemberEntity("paul@gmail.com", "paul", "1234");
        entityManager.persist(member);
    }
}

@Transactional은 해당 어노테이션이 붙어있는 메소드에 트랜잭션을 적용해준다. 따라서 메소드 전체가 성공해야 트랜잭션을 커밋하며, 메소드 실행 중 예외가 발생하면 트랜잭션을 롤백한다.

@Transactional어노테이션은 메소드 뿐만 아니라 클래스에도 붙일 수 있다. 이 경우 모든 메소드에 트랜잭션이 적용된다.

@Transactional
public class MemberService {
    // ...
}

참고로 Spring Data JPA를 사용하지 않는 경우 일반적으로 Dao클래스를 직접 정의하고 내부적으로 EntityManager를 사용하여 Data Access Layer를 구현했다.

public class MemberDao {

    protected EntityManager entityManager;

    @PersistenceContext
    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public void save(member MemberEntity) throws DataAccessException {
        entityManager.persist(member);
    }

    public MemberEntity get(Long id) {
        return MemberEntity entityManager.find(MemberEntity.class, id);
    }

    public void delete(member MemberEntity) throws DataAccessException {
        entityManager.remove(member);
    }

    // ...
} 

Spring Data JPA를 사용하는 경우 EntityManager를 직접 사용하기 보단 Spring Data JPA가 제공하는 JpaRepository 인터페이스를 구현한다.

public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
    // ...
}

JpaRepository는 내부적으로 Shared EntityManager를 사용하여 영속성 컨텍스트를 조작한다. 따라서 JpaRepository의 메소드를 호출하는 곳에 @Transaction 어노테이션을 추가해야한다.







 










@Service
public class MemberService {

    @Autowired
    private MemberRepository memberRepository;

    @Transactional
    public void join(String email, String name, String password) {
        // ...
    
        MemberEntity member = new Member(email, name, password);
        memberRepository.save(member);

        // ...
    }
}

@Transactional 어노테이션은 Unchecked Exception이 발생했을 때만 롤백한다. Checked Exception이 발생해도 롤백을 하고 싶으면 @Transactional(rollbackFor = Exception.class)로 지정해야한다.

# @Transactional과 테스트

테스트 클래스나 테스트 메소드에 @Transactional 어노테이션을 추가하면 테스트가 끝난 후 자동으로 롤백된다.

@SpringBootTest
public class Test {
    
    @Transcational
    public void test() {
        // ...
    }
}

@Transactional을 사용하지 않는 경우 다음과 같이 테스트 종료 후 데이터를 삭제하는 코드를 추가한다.

public class Test {

    @Autowired MemberRepository memberRepository;

    @AfterEach
    public void tearDown() {
        memberRepository.deleteAll();
    }

    @Transcational
    public void test() {
        // ...
    }
}

@SpringBootTestwebEnvironment속성을 SpringBootTest.WebEnvironment.RANDOM_PORT로 설정하면 @Transactional 어노테이션을 붙여도 롤백되지 않는다. 따라서 다음과 같이 롤백 코드를 직접 추가해야한다.



























 



@SpringBootTest(
     webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
class AuthControllerTest {

    @Autowired
    TestRestTemplate template;

    @Autowired
    MemberRepository memberRepository;

    @Nested
    @DisplayName("회원가입 테스트")
    class Join {

        @Test
        @DisplayName("회원가입 성공 테스트")
        void join() {
            JoinRequestDto request = JoinRequestDto.builder()
                    .email("Smith@gmail.com")
                    .name("Smith")
                    .nickname("Smith")
                    .password("4321Qwer32!!")
                    .build();
            ResponseEntity<JoinResponseDto> response = template.postForEntity("/auth/join", request, JoinResponseDto.class);
            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
            memberRepository.deleteById(response.getBody().getMemberId());
        }
    }

# @Commit, @Rollback

@Commit, @Rollback은 스프링 테스트 모듈에 포함된 어노테이션으로 테스트 환경에서 사용한다.

// build.gradle
dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // ...
}

테스트 환경에서 @Test, @Transactional을 함께 사용하는 경우 테스트가 끝나면 기본적으로 자동으로 롤백이 된다. 다음 두 코드는 동일하다.

@SpringBootTest
public class Test {

    @Autowired
    MemberRepository memberRepository;

    @Test
    @Transactional
    public test() {
        memberRepository.save(new MemberEntity("ronaldo@gmail.com", "ronaldo", 35));
        memberRepository.save(new MemberEntity("messi@gmail.com", "messi", 35));

        // ...
    }
}








 








@SpringBootTest
public class Test {

    @Autowired
    MemberRepository memberRepository;

    @Test
    @Transactional
    @Rollback(value = true)
    public test() {
        memberRepository.save(new MemberEntity("ronaldo@gmail.com", "ronaldo", 35));
        memberRepository.save(new MemberEntity("messi@gmail.com", "messi", 35));

        // ...
    }
}

@Rollback(value = false)로 설정하는 경우 테스트 종료 후 롤백되지 않도록 한다. 메소드에 붙이면 해당 메소드만 롤백이 된다.








 





@SpringBootTest
public class Test {

    // ...

    @Test
    @Transactional
    @Rollback(value = false)
    public test() {
        // ...
    }
}

클래스에 붙이면 모든 메소드에 적용된다.


 











@SpringBootTest
@Rollback(value = false)
public class Test {

    // ...

    @Test
    @Transactional
    public test() {
        // ...
    }
}

@Commit 어노테이션을 붙이면 테스트 종료 후 데이터베이스에도 반영되며, @Rollback(value = false)와 동일하다.





 



@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Rollback(false)
public @interface Commit {
}